<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
	<meta name="keywords" content="Seata、Dynamic、Config" />
	<meta name="description" content="讲述了 Seata 支持的多个配置中心是如何适配不同的动态配置订阅以及如何实现降级功能。" />
	<!-- 网页标签标题 -->
	<title>Seata 动态配置订阅与降级实现原理</title>
  <link rel="shortcut icon" href="/img/seata_logo_small.jpeg"/>
	<link rel="stylesheet" href="/build/blogDetail.css" />
</head>
<body>
	<div id="root"><div class="blog-detail-page" data-reactroot=""><header class="header-container header-container-normal"><div class="header-body"><a href="/zh-cn/index.html"><img class="logo" src="//img.alicdn.com/tfs/TB1gqL1w4D1gK0jSZFyXXciOVXa-1497-401.png"/></a><div class="search search-normal"><span class="icon-search"></span></div><span class="language-switch language-switch-normal">En</span><div class="header-menu"><img class="header-menu-toggle" src="https://img.alicdn.com/tfs/TB14eEmw7P2gK0jSZPxXXacQpXa-38-32.png"/><ul><li class="menu-item menu-item-normal"><a href="/zh-cn/index.html" target="_self">首页</a></li><li class="menu-item menu-item-normal"><a href="/zh-cn/docs/overview/what-is-seata.html" target="_self">文档</a></li><li class="menu-item menu-item-normal"><a href="/zh-cn/docs/developers/developers_dev.html" target="_self">开发者</a></li><li class="menu-item menu-item-normal menu-item-normal-active"><a href="/zh-cn/blog/index.html" target="_self">博客</a></li><li class="menu-item menu-item-normal"><a href="/zh-cn/community/index.html" target="_self">社区</a></li><li class="menu-item menu-item-normal"><a href="/zh-cn/blog/download.html" target="_self">下载</a></li></ul></div></div></header><section class="blog-content markdown-body"><h1>前言</h1>
<p>Seata 的动态降级需要结合配置中心的动态配置订阅功能。动态配置订阅，即通过配置中心监听订阅，根据需要读取已更新的缓存值，ZK、Apollo、Nacos 等第三方配置中心都有现成的监听器可实现动态刷新配置；动态降级，即通过动态更新指定配置参数值，使得 Seata 能够在运行过程中动态控制全局事务失效（目前只有 AT 模式有这个功能）。</p>
<p>那么 Seata 支持的多个配置中心是如何适配不同的动态配置订阅以及如何实现降级的呢？下面从源码的层面详细给大家讲解一番。</p>
<h1>动态配置订阅</h1>
<p>Seata 配置中心有一个监听器基准接口，它主要有一个抽象方法和 default 方法，如下：</p>
<p>io.seata.config.ConfigurationChangeListener</p>
<p><img src="https://gitee.com/objcoding/md-picture/raw/master/img/20191216212442.png" alt=""></p>
<p>该监听器基准接口主要有两个实现类型：</p>
<ol>
<li>实现注册配置订阅事件监听器：用于实现各种功能的动态配置订阅，比如 GlobalTransactionalInterceptor 实现了 ConfigurationChangeListener，根据动态配置订阅实现的动态降级功能；</li>
<li>实现配置中心动态订阅功能与适配：对于目前还没有动态订阅功能的 file 类型默认配置中心，可以实现该基准接口来实现动态配置订阅功能；对于阻塞订阅需要另起一个线程去执行，这时候可以实现该基准接口进行适配，还可以复用该基准接口的线程池；以及还有异步订阅，有订阅单个 key，有订阅多个 key 等等，我们都可以实现该基准接口以适配各个配置中心。</li>
</ol>
<h2>Nacos 动态订阅实现</h2>
<p>Nacos 有自己内部实现的监听器，因此直接直接继承它内部抽象监听器 AbstractSharedListener，实现如下：</p>
<p><img src="https://gitee.com/objcoding/md-picture/raw/master/img/20191223212237.png" alt=""></p>
<p>如上，</p>
<ul>
<li>dataId：为订阅的配置属性；</li>
<li>listener：配置订阅事件监听器，用于将外部传入的 listener 作为一个 wrapper，执行真正的变更逻辑。</li>
</ul>
<p>值得一提的是，nacos 并没有使用 ConfigurationChangeListener 实现自己的监听配置，一方面是因为 Nacos 本身已有监听订阅功能，不需要自己再去实现；另一方面因为 nacos 属于非阻塞式订阅，不需要复用 ConfigurationChangeListener 的线程池，即无需进行适配。</p>
<p>添加订阅：</p>
<p><img src="https://gitee.com/objcoding/md-picture/raw/master/img/20191223213347.png" alt=""></p>
<p>Nacos 配置中心为某个 dataId 添加订阅的逻辑很简单，用 dataId 和 listener 创建一个 NacosListener 调用 configService#addListener 方法，把 NacosListener 作为 dataId 的监听器，dataId 就实现了动态配置订阅功能。</p>
<h2>file 动态订阅实现</h2>
<p>以它的实现类 FileListener 举例子，它的实现逻辑如下：</p>
<p><img src="https://gitee.com/objcoding/md-picture/raw/master/img/20191215151642.png" alt=""></p>
<p>如上，</p>
<ul>
<li>
<p>dataId：为订阅的配置属性；</p>
</li>
<li>
<p>listener：配置订阅事件监听器，用于将外部传入的 listener 作为一个 wrapper，执行真正的变更逻辑，这里特别需要注意的是，<strong>该监听器与 FileListener 同样实现了 ConfigurationChangeListener 接口，只不过 FileListener 是用于给 file 提供动态配置订阅功能，而 listener 用于执行配置订阅事件</strong>；</p>
</li>
<li>
<p>executor：用于处理配置变更逻辑的线程池，在 ConfigurationChangeListener#onProcessEvent 方法中用到。</p>
</li>
</ul>
<p><strong>FileListener#onChangeEvent 方法的实现让 file 具备了动态配置订阅的功能</strong>，它的逻辑如下：</p>
<p>无限循环获取订阅的配置属性当前的值，从缓存中获取旧的值，判断是否有变更，如果有变更就执行外部传入 listener 的逻辑。</p>
<p>ConfigurationChangeEvent 用于保存配置变更的事件类，它的成员属性如下：</p>
<p><img src="https://gitee.com/objcoding/md-picture/raw/master/img/20191215175232.png" alt=""></p>
<p>这里的 getConfig 方法是如何感知 file 配置的变更呢？我们点进去，发现它最终的逻辑如下：</p>
<p><img src="https://gitee.com/objcoding/md-picture/raw/master/img/20191215162713.png" alt=""></p>
<p>发现它是创建一个 future 类，然后包装成一个 Runnable 放入线程池中异步执行，最后调用 get 方法阻塞获取值，那么我们继续往下看：</p>
<p><img src="https://gitee.com/objcoding/md-picture/raw/master/img/20191215170908.png" alt=""></p>
<p>allowDynamicRefresh：动态刷新配置开关；</p>
<p>targetFileLastModified：file 最后更改的时间缓存。</p>
<p>以上逻辑：</p>
<p>获取 file 最后更新的时间值 tempLastModified，然后对比对比缓存值 targetFileLastModified，如果 tempLastModified &gt; targetFileLastModified，说明期间配置有更改过，这时就重新加载 file 实例，替换掉旧的 fileConfig，使得后面的操作能够获取到最新的配置值。</p>
<p>添加一个配置属性监听器的逻辑如下：</p>
<p><img src="https://gitee.com/objcoding/md-picture/raw/master/img/20191215161103.png" alt=""></p>
<p>configListenersMap 为 FileConfiguration 的配置监听器缓存，它的数据结构如下：</p>
<pre><code class="language-java">ConcurrentMap&lt;String<span class="hljs-comment">/*dataId*/</span>, Set&lt;ConfigurationChangeListener&gt;&gt; configListenersMap
</code></pre>
<p>从数据结构上可看出，每个配置属性可关联多个事件监听器。</p>
<p>最终执行 onProcessEvent 方法，这个是监听器基准接口里面的 default 方法，它会调用 onChangeEvent 方法，即最终会调用 FileListener 中的实现。</p>
<h1>动态降级</h1>
<p>有了以上的动态配置订阅功能，我们只需要实现 ConfigurationChangeListener 监听器，就可以做各种各种的功能，目前 Seata 只有动态降级有用到动态配置订阅的功能。</p>
<p>在「<a href="https://mp.weixin.qq.com/s/n9MHk47zSsFQmV-gBq_P1A">Seata AT 模式启动源码分析</a>」这篇文章中讲到，Spring 集成 Seata 的项目中，在 AT 模式启动时，会用 用GlobalTransactionalInterceptor 代替了被 GlobalTransactional 和 GlobalLock 注解的方法，GlobalTransactionalInterceptor 实现了 MethodInterceptor，最终会执行 invoker 方法，那么想要实现动态降级，就可以在这里做手脚。</p>
<ul>
<li>在 GlobalTransactionalInterceptor 中加入一个成员变量：</li>
</ul>
<pre><code class="language-java"><span class="hljs-keyword">private</span> <span class="hljs-keyword">volatile</span> <span class="hljs-keyword">boolean</span> disable; 
</code></pre>
<p>在构造函数中进行初始化赋值：</p>
<p><img src="https://gitee.com/objcoding/md-picture/raw/master/img/20191215173221.png" alt=""></p>
<p>ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION（service.disableGlobalTransaction）这个参数目前有两个功能：</p>
<ol>
<li>在启动时决定是否开启全局事务；</li>
<li>在开启全局事务后，决定是否降级。</li>
</ol>
<ul>
<li>实现 ConfigurationChangeListener：</li>
</ul>
<p><img src="https://gitee.com/objcoding/md-picture/raw/master/img/20191215173358.png" alt=""></p>
<p>这里的逻辑简单，就是判断监听事件是否属于 ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION 配置属性，如果是，直接更新 disable 值。</p>
<ul>
<li>接下来在 GlobalTransactionalInterceptor#invoke 中做点手脚</li>
</ul>
<p><img src="https://gitee.com/objcoding/md-picture/raw/master/img/20191215174155.png" alt=""></p>
<p>如上，disable = true 时，不执行全局事务与全局锁。</p>
<ul>
<li>配置中心订阅降级监听器</li>
</ul>
<p>io.seata.spring.annotation.GlobalTransactionScanner#wrapIfNecessary</p>
<p><img src="https://gitee.com/objcoding/md-picture/raw/master/img/20191215174409.png" alt=""></p>
<p>在 Spring AOP 进行 wrap 逻辑过程中，当前配置中心将订阅降级事件监听器。</p>
<h1>作者简介</h1>
<p>张乘辉，目前就职于中通科技信息中心技术平台部，担任 Java 工程师，主要负责中通消息平台与全链路压测项目的研发，热爱分享技术，微信公众号「后端进阶」作者，技术博客（<a href="https://objcoding.com/">https://objcoding.com/</a>）博主，Seata Contributor，GitHub ID：objcoding。</p>
</section><footer class="footer-container"><div class="footer-body"><img src="//img.alicdn.com/tfs/TB1dGrSwVT7gK0jSZFpXXaTkpXa-4802-1285.png"/><p class="docsite-power">website powered by docsite</p><div class="cols-container"><div class="col col-12"><h3>愿景</h3><p>Seata 是一款阿里巴巴开源的分布式事务解决方案，致力于在微服务架构下提供高性能和简单易用的分布式事务服务。</p></div><div class="col col-6"><dl><dt>文档</dt><dd><a href="/zh-cn/docs/overview/what-is-seata.html" target="_self">Seata 是什么？</a></dd><dd><a href="/zh-cn/docs/user/quickstart.html" target="_self">快速开始</a></dd><dd><a href="https://github.com/seata/seata.github.io/issues/new" target="_self">报告文档问题</a></dd><dd><a href="https://github.com/seata/seata.github.io" target="_self">在Github上编辑此文档</a></dd></dl></div><div class="col col-6"><dl><dt>资源</dt><dd><a href="/zh-cn/blog/index.html" target="_self">博客</a></dd><dd><a href="/zh-cn/community/index.html" target="_self">社区</a></dd></dl></div></div><div class="copyright"><span>Copyright © 2019 Seata</span></div></div></footer></div></div>
	<script src="https://f.alicdn.com/react/15.4.1/react-with-addons.min.js"></script>
	<script src="https://f.alicdn.com/react/15.4.1/react-dom.min.js"></script>
	<script>
		window.rootPath = '';
  </script>
	<script src="/build/blogDetail.js"></script>
	<script>
    var _hmt = _hmt || [];
    (function() {
      var hm = document.createElement("script");
      hm.src = "https://hm.baidu.com/hm.js?104e73ef0c18b416b27abb23757ed8ee";
      var s = document.getElementsByTagName("script")[0];
      s.parentNode.insertBefore(hm, s);
    })();
    </script>
</body>
</html>
